package tr.com.softoval.beanscanner.generator;

import java.util.List;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.SystemPropertyUtils;

import tr.com.softoval.beanscanner.api.BeanCandidateException;
import tr.com.softoval.beanscanner.config.BeanCandidateClasspath;
import tr.com.softoval.beanscanner.config.BeanCandidateLiterals;
import tr.com.softoval.beanscanner.util.Constants;
import tr.com.softoval.beanscanner.util.Utils;

public class BeanDefinitionGenerator {
	private BeanCandidateLiterals 			beanCandidateLiterals = BeanCandidateLiterals.getInstance();
	private BeanCandidateClasspath 			beanCandidateClasspath = BeanCandidateClasspath.getInstance();
	private BeanCandidateFilter 			beanCandidateFilter = BeanCandidateFilter.getInstance();
	private MetadataReaderFactory    		metadataReaderFactory;
	private BeanDefinitionRegistry 			beanDefinitionRegistry;
	private ResourcePatternResolver         resourcePatternResolver;
	private Integer							generatedBeanDefintionCount = 0;

	public static final BeanDefinitionGenerator getInstance() {
		return new BeanDefinitionGenerator();
	}
	
	private BeanDefinitionGenerator() {}
	
	
	public void setBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		this.beanDefinitionRegistry = BeanDefinitionRegistry.class.cast(beanFactory);
		this.controlsOverConfigurations();
		this.generateBeansOnContext();
	}
	
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
		this.beanCandidateFilter.setMetadataReaderFactory(new CachingMetadataReaderFactory(resourceLoader));
		this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
	}
	
	private void controlsOverConfigurations() {
		if (Utils.isEmptyList(beanCandidateClasspath.getBaseLocations())) {
			throw new BeanCandidateException("At least one (1) base location must be provied.");
		}
		
		if (Utils.isEmptyList(beanCandidateClasspath.getClassPathScanner())) {
			throw new BeanCandidateException("At least one (1) classpath scan pattern must be provided. Exampe : /**/*.class ");
		}
		
		if (Utils.isEmptyLiteral(beanCandidateLiterals)) {
			throw new BeanCandidateException("At least one (1) literal(Abstract,Interface,Annotation) must be provided.");
		}
	}
	
	private void generateBeansOnContext() throws BeanCandidateException {
		Utils.log("Bean Candidate Scanner started.");
		Utils.log("Base locations in classpath will be scanned : " + Utils.listToString(beanCandidateClasspath.getBaseLocations()));
		Utils.log("Classpath search patterns : " + Utils.listToString(beanCandidateClasspath.getClassPathScanner()));
		Utils.log("AbstractClass literals : " + Utils.listToString(beanCandidateLiterals.getAbstractLiterals()));
		Utils.log("Interface literals : " + Utils.listToString(beanCandidateLiterals.getInterfaceLiterals()));
		Utils.log("Annotation literals : " + Utils.listToString(beanCandidateLiterals.getAnnotationLiterals()));
		try {
			Utils.log("Filters are configuring.");
			this.beanCandidateFilter.setBeanCandidateLiterals(beanCandidateLiterals);
			this.beanCandidateFilter.generateDeclaredFilters();
			Utils.log("All ScanFilters are configured successfully.");
			this.generateAbstractDefinitionOnContext();
		} catch (ClassNotFoundException e) {
			throw new BeanCandidateException(e);
		}
		Utils.log("Starting classpath scan.");
		for (String currentPackage : beanCandidateClasspath.getBaseLocations()) {
			try {
				for (String pattern : beanCandidateClasspath.getClassPathScanner()) {
					String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					        ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(currentPackage)) +
					        "/" + pattern;
					Utils.log("Scanning pattern : " + packageSearchPath);
					
					Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
					Utils.log(resources.length + " file system resource found.");
					for (Resource resource : resources) {
						if (resource.isReadable()) {
							MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
							
							Class<?> clazz = Class.forName(metadataReader.getClassMetadata().getClassName());
							
							if (beanCandidateFilter.isInstantiatableClass(clazz) == Boolean.FALSE) {
								continue;
							} else if (beanCandidateFilter.isMatchOverFilters(metadataReader) == Boolean.FALSE) {
								continue;
							} else {
								GenericBeanDefinition def = this.prepareBeanDefinition(clazz, false);
								this.registerBeanOnContext(def);
							}
						}
					}
				}
			} catch(Exception e) {
				throw new BeanCandidateException(e);
			}
		}
		Utils.log("Classpath scan is finished.");
		Utils.log("Bean Candidate Scanner finished. [" + this.generatedBeanDefintionCount + "] bean created on Context.");
	}
	
	
	private void generateAbstractDefinitionOnContext() throws ClassNotFoundException {
		Utils.log("Abstract Definitions are instantiating on Context.");
		for (String abstractDefition : beanCandidateLiterals.getAbstractLiterals()) {
			Class<?> clazz = Class.forName(abstractDefition);
			GenericBeanDefinition def = this.prepareBeanDefinition(clazz, true);
			this.registerBeanOnContext(def);
		}
		Utils.log("Abstract Definitions Intantiating finished.");
	}
	
	
	private GenericBeanDefinition prepareBeanDefinition(Class<?> clazz, boolean abstractDefition) {
		GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
		beanDefinition.setBeanClass(clazz);
		beanDefinition.setLazyInit(false);
		beanDefinition.setAbstract(abstractDefition);
		beanDefinition.setAutowireCandidate(true);
		beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		return beanDefinition;
	}
	
	
	private void registerBeanOnContext(GenericBeanDefinition definition) {
		beanDefinitionRegistry.registerBeanDefinition(definition.getBeanClassName(), definition);
		this.generatedBeanDefintionCount++;
		Utils.log("REGISTERE-BEAN on Context ["+ definition.getBeanClassName() + "]");
	}
	
	
	public void setGeneratorProperty(String property, List<String> literals) {
		if (Constants.ABSTRACT_DEFINITION.equals(property)) {
			beanCandidateLiterals.setAbstractLiterals(literals);
		} else if (Constants.ANNOTATION_DEFINITION.equals(property)) {
			beanCandidateLiterals.setAnnotationLiterals(literals);
		} else if (Constants.INTERFACE_DEFINITION.equals(property)) {
			beanCandidateLiterals.setInterfaceLiterals(literals);
		} else if (Constants.BASE_LOCATION.equals(property)) {
			beanCandidateClasspath.setBaseLocations(literals);
		} else if (Constants.SEARCH_PATTERN.equals(property)) {
			beanCandidateClasspath.setClassPathScanner(literals);
		}
	}

}
